﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Windows.Media.Media3D10
{
    /// <summary>
    /// Liste des formes de base pour la tesselation de sphere
    /// </summary>
    public enum BaseMesh : int
    { 
        /// <summary>
        /// Tetrahèdre
        /// </summary>
        Tetrahedron = 0,
        /// <summary>
        /// Octahèdre
        /// </summary>
        Octahedron = 1,
        /// <summary>
        /// Icosahèdre
        /// </summary>
        Icosahedron = 2,
    }

    /// <summary>
    /// Fourni des méthodes pour générer des sphères par tesselation
    /// </summary>
    public static class SphereTesselator
    {
        private static MeshGeometry3D10[] _baseMeshes = CreateBaseMeshes();

        /// <summary>
        /// Génére une sphère en utilisant la méthode de Campanus
        /// </summary>
        /// <param name="radius">Rayon de la sphère désirée</param>
        /// <param name="tDiv">Nombre de divisions de chaque du horizontal</param>
        /// <param name="zDiv">Nombre de divisions du plan vertical</param>
        /// <returns>Retourne un <see cref="MeshGeometry3D10"/> représentant la sphère générée.</returns>
        public static MeshGeometry3D10 TesselateCampanus(float radius, int tDiv, int zDiv)
        {
            MeshGeometry3D10 returnValue = new MeshGeometry3D10();

            float maxTheta = 2 *(float) Math.PI;
            float minz = -radius;
            float maxz = radius;

            float dt = maxTheta / tDiv;
            float dz = (maxz - minz) / zDiv;

            for (int zi = 0; zi <= zDiv; zi++)
            {
                float z = minz + zi * dz;

                for (int ti = 0; ti <= tDiv; ti++)
                {
                    float t = ti * dt;

                    returnValue.Positions.Add(GetPosition(radius, t, z));
                    returnValue.Normals.Add(GetNormal(t, z));
                    returnValue.TextureCoordinates.Add(GetTextureCoordinates(radius, t, z));
                }
            }

            for (int zi = 0; zi < zDiv; zi++)
            {
                for (int ti = 0; ti < tDiv; ti++)
                {
                    int x0 = ti;
                    int x1 = (ti + 1);
                    int z0 = zi * (tDiv + 1);
                    int z1 = (zi + 1) * (tDiv + 1);

                    returnValue.TriangleIndices.Add(x0 + z0);
                    returnValue.TriangleIndices.Add(x0 + z1);
                    returnValue.TriangleIndices.Add(x1 + z0);

                    returnValue.TriangleIndices.Add(x1 + z0);
                    returnValue.TriangleIndices.Add(x0 + z1);
                    returnValue.TriangleIndices.Add(x1 + z1);
                }
            }

            return returnValue;
        }

        /// <summary>
        /// Génère une isosphère
        /// </summary>
        /// <param name="radius">Rayon de la sphère désirée</param>
        /// <param name="iteration">Nombre d'itération à réaliser lors de la tesselation</param>
        /// <param name="baseMesh">Forme de base à utiliser</param>
        /// <returns>Retourne un <see cref="MeshGeometry3D10"/> représentant la sphère générée.</returns>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="iteration"/> n'est pas dans les valeurs admissibles</exception>
        public static MeshGeometry3D10 TesselateGeodesic(float radius, int iteration, BaseMesh baseMesh)
        {
            if (iteration < 1 || iteration > (7 - (int)baseMesh))
                throw new ArgumentOutOfRangeException("iteration");

            MeshGeometry3D10 mesh = _baseMeshes[(int)baseMesh];

            bool revert = false;
            for (int i = 0; i < iteration; i++)
            {
                revert = !revert;
                mesh = Subdivide(mesh, revert);
            }

            CorrectArtifact(mesh);

            for (int i = 0; i < mesh.Positions.Count; i++)
                mesh.Positions[i] = mesh.Positions[i] * radius;

            return mesh;
        }

        private static MeshGeometry3D10 Subdivide(MeshGeometry3D10 baseMesh, bool revertTriangleIndices)
        {
            int facesCount = baseMesh.TriangleIndices.Count / 3;    //Nombre de faces

            MeshGeometry3D10 returnValue = new MeshGeometry3D10();

            //On copie les vecteurs déjà existants
            foreach (SlimDX.Vector3 pos in baseMesh.Positions)
            {
                returnValue.Positions.Add(pos);
            }
   
            for (int i = 0; i < facesCount; i++)
			{			 
                //On récupère les indices des vecteurs de la face actuelle
                int indiceA = baseMesh.TriangleIndices[i * 3];
                int indiceB = baseMesh.TriangleIndices[i * 3 + 1];
                int indiceC = baseMesh.TriangleIndices[i * 3 + 2];
    
                //On récupère les vecteurs de la face actuelle
                SlimDX.Vector3 verticeA = baseMesh.Positions[indiceA];
                SlimDX.Vector3 verticeB = baseMesh.Positions[indiceB];
                SlimDX.Vector3 verticeC = baseMesh.Positions[indiceC];
    
                //On calcule les vecteurs d'intersections
                SlimDX.Vector3 verticea = (verticeA + verticeB) / 2;
                SlimDX.Vector3 verticeb = (verticeB + verticeC) / 2;
                SlimDX.Vector3 verticec = (verticeC + verticeA) / 2;

                //On normalize les vecteurs d'intersection
                verticea.Normalize();
                verticeb.Normalize();
                verticec.Normalize();
        
                //Vérifie si les vecteurs d'intersections existent déjà dans la liste des positions.
                //Le cas échéant, on utilise le vecteur déjà pour la suite de l'algorithme.
                //Sinon, on ajoute le vecteur d'intersection à la liste des positions
                SlimDX.Vector3 duplicatedVerticea = returnValue.Positions.FirstOrDefault(pos => pos.Equals(verticea));
                SlimDX.Vector3 duplicatedVerticeb = returnValue.Positions.FirstOrDefault(pos => pos.Equals(verticeb));
                SlimDX.Vector3 duplicatedVerticec = returnValue.Positions.FirstOrDefault(pos => pos.Equals(verticec));

                if (!duplicatedVerticea.Equals(SlimDX.Vector3.Zero)
                    && returnValue.Positions.Any(pos => pos.X > duplicatedVerticea.X))
                    verticea = duplicatedVerticea;
                else
                    returnValue.Positions.Add(verticea);

                if (!duplicatedVerticeb.Equals(SlimDX.Vector3.Zero)
                    && returnValue.Positions.Any(pos => pos.X > duplicatedVerticeb.X))
                    verticeb = duplicatedVerticeb;
                else
                    returnValue.Positions.Add(verticeb);

                if (!duplicatedVerticec.Equals(SlimDX.Vector3.Zero)
                    && returnValue.Positions.Any(pos => pos.X > duplicatedVerticec.X))
                    verticec = duplicatedVerticec;
                else
                    returnValue.Positions.Add(verticec);

                //On récupère les indices des vecteurs d'intersections
                int indicea = returnValue.Positions.IndexOf(verticea);
                int indiceb = returnValue.Positions.IndexOf(verticeb);
                int indicec = returnValue.Positions.IndexOf(verticec);

                //On crée les données pour les faces nouvellements créees
                if (revertTriangleIndices)
                {
                    returnValue.TriangleIndices.Add(indicec);
                    returnValue.TriangleIndices.Add(indicea);
                    returnValue.TriangleIndices.Add(indiceA);

                    returnValue.TriangleIndices.Add(indiceb);
                    returnValue.TriangleIndices.Add(indiceB);
                    returnValue.TriangleIndices.Add(indicea);

                    returnValue.TriangleIndices.Add(indiceC);
                    returnValue.TriangleIndices.Add(indiceb);
                    returnValue.TriangleIndices.Add(indicec);

                    returnValue.TriangleIndices.Add(indicec);
                    returnValue.TriangleIndices.Add(indiceb);
                    returnValue.TriangleIndices.Add(indicea);
                }
                else
                {
                    returnValue.TriangleIndices.Add(indiceA);
                    returnValue.TriangleIndices.Add(indicea);
                    returnValue.TriangleIndices.Add(indicec);

                    returnValue.TriangleIndices.Add(indicea);
                    returnValue.TriangleIndices.Add(indiceB);
                    returnValue.TriangleIndices.Add(indiceb);

                    returnValue.TriangleIndices.Add(indicec);
                    returnValue.TriangleIndices.Add(indiceb);
                    returnValue.TriangleIndices.Add(indiceC);

                    returnValue.TriangleIndices.Add(indicea);
                    returnValue.TriangleIndices.Add(indiceb);
                    returnValue.TriangleIndices.Add(indicec);
                }
			}

            //On crée la liste des normales
            foreach (SlimDX.Vector3 pos in returnValue.Positions)
            {
                returnValue.Normals.Add(pos);
            }

            //On crée la liste des coordonnées de texture
            foreach (SlimDX.Vector3 pos in returnValue.Positions)
            {
                SlimDX.Vector2 coord = GetTextureCoordinates(pos);
                returnValue.TextureCoordinates.Add(coord);
            }

            return returnValue;
        }

        private static void CorrectArtifact(MeshGeometry3D10 mesh)
        {
            int facesCount = mesh.TriangleIndices.Count / 3;

            List<SlimDX.Vector3> vectorsToCorrect = mesh.Positions.Where(pos => pos.Y == 0 && pos.X >= 0.5f).ToList();

            foreach (SlimDX.Vector3 vect in vectorsToCorrect)
            {
                int oldIndex = mesh.Positions.IndexOf(vect);
                mesh.Positions.Add(new SlimDX.Vector3(vect.X, vect.Y, vect.Z));
                mesh.Normals.Add(new SlimDX.Vector3(vect.X, vect.Y, vect.Z));
                mesh.TextureCoordinates.Add(new SlimDX.Vector2(1.0f, -vect.Z / 2.0f + 0.5f));
                int newIndex = mesh.Positions.Count - 1;

                for (int i = 0; i < facesCount; i++)
                {
                    int indicea = mesh.TriangleIndices[3 * i];
                    int indiceb = mesh.TriangleIndices[3 * i + 1];
                    int indicec = mesh.TriangleIndices[3 * i + 2];

                    if (indicea == oldIndex && (mesh.TextureCoordinates[indiceb].X > 0.5 || mesh.TextureCoordinates[indicec].X > 0.5))
                        mesh.TriangleIndices[3 * i] = newIndex;
                    if (indiceb == oldIndex && (mesh.TextureCoordinates[indicec].X > 0.5 || mesh.TextureCoordinates[indicea].X > 0.5))
                        mesh.TriangleIndices[3 * i + 1] = newIndex;
                    if (indicec == oldIndex && (mesh.TextureCoordinates[indicea].X > 0.5 || mesh.TextureCoordinates[indiceb].X > 0.5))
                        mesh.TriangleIndices[3 * i + 2] = newIndex;
                }   
            }
        }

        private static SlimDX.Vector3 GetPosition(float radius, float t, float z)
        {
            float r = (float)Math.Sqrt(radius * radius - z * z);
            float y = r * (float)Math.Sin(t);
            float x = r * (float)Math.Cos(t);

            return new SlimDX.Vector3((float)x, (float)y, (float)z);
        }

        private static SlimDX.Vector3 GetNormal(float t, float z)
        {
            float y = (float)Math.Sin(t);
            float x = (float)Math.Cos(t);

            return new SlimDX.Vector3(x, y, z);
        }

        private static SlimDX.Vector2 GetTextureCoordinates(float radius, float t, float z)
        {
            float u = t / (2 * (float)Math.PI);
            float v = -z / (2 * radius) + 0.5f;

            return new SlimDX.Vector2(u, v);
        }

        private static SlimDX.Vector2 GetTextureCoordinates(SlimDX.Vector3 vector)
        {
            SlimDX.Vector3 normalizedVector = vector.Length() == 1 ? vector : SlimDX.Vector3.Normalize(vector);
            float atan = (float)Math.Atan2(normalizedVector.Y, normalizedVector.X);
            atan /= (2 * (float)Math.PI);
            if (atan < 0) atan += 1;
            float u = atan;
            float v = -vector.Z / 2.0f + 0.5f;
            return new SlimDX.Vector2(u, v);
        }

        private static MeshGeometry3D10[] CreateBaseMeshes()
        {
            MeshGeometry3D10[] baseMeshes = new MeshGeometry3D10[3];

            MeshGeometry3D10 tetrahedron = CreateTetrahedron();
            MeshGeometry3D10 octahedron = CreateOctahedron();
            MeshGeometry3D10 icosahedron = CreateIcosahedron();
            
            baseMeshes[(int)BaseMesh.Tetrahedron] = tetrahedron;
            baseMeshes[(int)BaseMesh.Octahedron] = octahedron;
            baseMeshes[(int)BaseMesh.Icosahedron] = icosahedron;

            return baseMeshes;
        }

        private static MeshGeometry3D10 CreateTetrahedron()
        {
            float sqrt_3 = (float)Math.Sqrt(3.0);

            MeshGeometry3D10 tetrahedron = new MeshGeometry3D10();
            tetrahedron.Positions.Add(new SlimDX.Vector3(sqrt_3, sqrt_3, sqrt_3));
            tetrahedron.Positions.Add(new SlimDX.Vector3(-sqrt_3, -sqrt_3, sqrt_3));
            tetrahedron.Positions.Add(new SlimDX.Vector3(-sqrt_3, sqrt_3, -sqrt_3));
            tetrahedron.Positions.Add(new SlimDX.Vector3(sqrt_3, -sqrt_3, -sqrt_3));

            tetrahedron.TriangleIndices.Add(0);
            tetrahedron.TriangleIndices.Add(1);
            tetrahedron.TriangleIndices.Add(2);

            tetrahedron.TriangleIndices.Add(0);
            tetrahedron.TriangleIndices.Add(3);
            tetrahedron.TriangleIndices.Add(1);

            tetrahedron.TriangleIndices.Add(2);
            tetrahedron.TriangleIndices.Add(1);
            tetrahedron.TriangleIndices.Add(3);

            tetrahedron.TriangleIndices.Add(3);
            tetrahedron.TriangleIndices.Add(0);
            tetrahedron.TriangleIndices.Add(2);

            return tetrahedron;
        }

        private static MeshGeometry3D10 CreateOctahedron()
        {
            MeshGeometry3D10 octahedron = new MeshGeometry3D10();
            octahedron.Positions.Add(new SlimDX.Vector3(1, 0, 0));
            octahedron.Positions.Add(new SlimDX.Vector3(-1, 0, 0));
            octahedron.Positions.Add(new SlimDX.Vector3(0, 1, 0));
            octahedron.Positions.Add(new SlimDX.Vector3(0, -1, 0));
            octahedron.Positions.Add(new SlimDX.Vector3(0, 0, 1));
            octahedron.Positions.Add(new SlimDX.Vector3(0, 0, -1));

            octahedron.TriangleIndices.Add(0);
            octahedron.TriangleIndices.Add(4);
            octahedron.TriangleIndices.Add(2);

            octahedron.TriangleIndices.Add(2);
            octahedron.TriangleIndices.Add(4);
            octahedron.TriangleIndices.Add(1);

            octahedron.TriangleIndices.Add(1);
            octahedron.TriangleIndices.Add(4);
            octahedron.TriangleIndices.Add(3);

            octahedron.TriangleIndices.Add(3);
            octahedron.TriangleIndices.Add(4);
            octahedron.TriangleIndices.Add(0);

            octahedron.TriangleIndices.Add(0);
            octahedron.TriangleIndices.Add(2);
            octahedron.TriangleIndices.Add(5);

            octahedron.TriangleIndices.Add(2);
            octahedron.TriangleIndices.Add(1);
            octahedron.TriangleIndices.Add(5);

            octahedron.TriangleIndices.Add(1);
            octahedron.TriangleIndices.Add(3);
            octahedron.TriangleIndices.Add(5);

            octahedron.TriangleIndices.Add(3);
            octahedron.TriangleIndices.Add(0);
            octahedron.TriangleIndices.Add(5);

            return octahedron;
        }

        private static MeshGeometry3D10 CreateIcosahedron()
        {
            MeshGeometry3D10 icosahedron = new MeshGeometry3D10();

            float t = (1 + (float)Math.Sqrt(5.0)) / 2;
            float tau = t / (float)Math.Sqrt(1 + t * t);
            float one = 1 / (float)Math.Sqrt(1 + t * t);

            icosahedron.Positions.Add(new SlimDX.Vector3(tau, one, 0));
            icosahedron.Positions.Add(new SlimDX.Vector3(-tau, one, 0));
            icosahedron.Positions.Add(new SlimDX.Vector3(-tau, -one, 0));
            icosahedron.Positions.Add(new SlimDX.Vector3(tau, -one, 0));
            icosahedron.Positions.Add(new SlimDX.Vector3(one, 0, tau));
            icosahedron.Positions.Add(new SlimDX.Vector3(one, 0, -tau));
            icosahedron.Positions.Add(new SlimDX.Vector3(-one, 0, -tau));
            icosahedron.Positions.Add(new SlimDX.Vector3(-one, 0, tau));
            icosahedron.Positions.Add(new SlimDX.Vector3(0, tau, one));
            icosahedron.Positions.Add(new SlimDX.Vector3(0, -tau, one));
            icosahedron.Positions.Add(new SlimDX.Vector3(0, -tau, -one));
            icosahedron.Positions.Add(new SlimDX.Vector3(0, tau, -one));

            icosahedron.TriangleIndices.Add(4);
            icosahedron.TriangleIndices.Add(8);
            icosahedron.TriangleIndices.Add(7);

            icosahedron.TriangleIndices.Add(4);
            icosahedron.TriangleIndices.Add(7);
            icosahedron.TriangleIndices.Add(9);

            icosahedron.TriangleIndices.Add(5);
            icosahedron.TriangleIndices.Add(6);
            icosahedron.TriangleIndices.Add(11);

            icosahedron.TriangleIndices.Add(5);
            icosahedron.TriangleIndices.Add(10);
            icosahedron.TriangleIndices.Add(6);

            icosahedron.TriangleIndices.Add(0);
            icosahedron.TriangleIndices.Add(4);
            icosahedron.TriangleIndices.Add(3);

            icosahedron.TriangleIndices.Add(0);
            icosahedron.TriangleIndices.Add(3);
            icosahedron.TriangleIndices.Add(5);

            icosahedron.TriangleIndices.Add(2);
            icosahedron.TriangleIndices.Add(7);
            icosahedron.TriangleIndices.Add(1);

            icosahedron.TriangleIndices.Add(2);
            icosahedron.TriangleIndices.Add(1);
            icosahedron.TriangleIndices.Add(6);

            icosahedron.TriangleIndices.Add(8);
            icosahedron.TriangleIndices.Add(0);
            icosahedron.TriangleIndices.Add(11);

            icosahedron.TriangleIndices.Add(8);
            icosahedron.TriangleIndices.Add(11);
            icosahedron.TriangleIndices.Add(1);

            icosahedron.TriangleIndices.Add(9);
            icosahedron.TriangleIndices.Add(10);
            icosahedron.TriangleIndices.Add(3);

            icosahedron.TriangleIndices.Add(9);
            icosahedron.TriangleIndices.Add(2);
            icosahedron.TriangleIndices.Add(10);

            icosahedron.TriangleIndices.Add(8);
            icosahedron.TriangleIndices.Add(4);
            icosahedron.TriangleIndices.Add(0);

            icosahedron.TriangleIndices.Add(11);
            icosahedron.TriangleIndices.Add(0);
            icosahedron.TriangleIndices.Add(5);

            icosahedron.TriangleIndices.Add(4);
            icosahedron.TriangleIndices.Add(9);
            icosahedron.TriangleIndices.Add(3);

            icosahedron.TriangleIndices.Add(5);
            icosahedron.TriangleIndices.Add(3);
            icosahedron.TriangleIndices.Add(10);

            icosahedron.TriangleIndices.Add(7);
            icosahedron.TriangleIndices.Add(8);
            icosahedron.TriangleIndices.Add(1);

            icosahedron.TriangleIndices.Add(6);
            icosahedron.TriangleIndices.Add(1);
            icosahedron.TriangleIndices.Add(11);

            icosahedron.TriangleIndices.Add(7);
            icosahedron.TriangleIndices.Add(2);
            icosahedron.TriangleIndices.Add(9);

            icosahedron.TriangleIndices.Add(6);
            icosahedron.TriangleIndices.Add(10);
            icosahedron.TriangleIndices.Add(2);

            return icosahedron;
        }
    }
}
